/**
 ******************************************************************************
 *
 * @file       core.cpp
 * @author     The OpenPilot Team, http://www.openpilot.org Copyright (C) 2012.
 * @brief
 * @see        The GNU Public License (GPL) Version 3
 * @defgroup   OPMapWidget
 * @{
 *
 *****************************************************************************/
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
#include "core.h"

#ifdef DEBUG_CORE
qlonglong internals::Core::debugcounter = 0;
#endif

using namespace projections;

namespace internals {
Core::Core() : MouseWheelZooming(false), currentPosition(0, 0), currentPositionPixel(0, 0), LastLocationInBounds(-1, -1), sizeOfMapArea(0, 0)
    , minOfTiles(0, 0), maxOfTiles(0, 0), zoom(0), isDragging(false), TooltipTextPadding(10, 10), loaderLimit(5), maxzoom(21), runningThreads(0), started(false)
{
    mousewheelzoomtype = MouseWheelZoomType::MousePositionAndCenter;
    SetProjection(new MercatorProjection());
    this->setAutoDelete(false);
    ProcessLoadTaskCallback.setMaxThreadCount(10);
    renderOffset = Point(0, 0);
    dragPoint    = Point(0, 0);
    CanDragMap   = true;
    tilesToload  = 0;
    OPMaps::Instance();
}
Core::~Core()
{
    ProcessLoadTaskCallback.waitForDone();
}

void Core::run()
{
    MrunningThreads.lock();
    ++runningThreads;
    MrunningThreads.unlock();
#ifdef DEBUG_CORE
    qlonglong debug;
    Mdebug.lock();
    debug = ++debugcounter;
    Mdebug.unlock();
    qDebug() << "core:run" << " ID=" << debug;
#endif // DEBUG_CORE
    bool last = false;

    LoadTask task;

    MtileLoadQueue.lock();
    {
        if (tileLoadQueue.count() > 0) {
            task = tileLoadQueue.dequeue();
            {
                last = (tileLoadQueue.count() == 0);
#ifdef DEBUG_CORE
                qDebug() << "TileLoadQueue: " << tileLoadQueue.count() << " Point:" << task.Pos.ToString() << " ID=" << debug;;
#endif // DEBUG_CORE
            }
        }
    }
    MtileLoadQueue.unlock();

    if (task.HasValue()) {
        if (loaderLimit.tryAcquire(1, OPMaps::Instance()->Timeout)) {
            MtileToload.lock();
            --tilesToload;
            MtileToload.unlock();
#ifdef DEBUG_CORE
            qDebug() << "loadLimit semaphore aquired " << loaderLimit.available() << " ID=" << debug << " TASK=" << task.Pos.ToString() << " " << task.Zoom;
#endif // DEBUG_CORE

            {
#ifdef DEBUG_CORE
                qDebug() << "task as value, begining get" << " ID=" << debug;;
#endif // DEBUG_CORE
                {
                    Tile *m = Matrix.TileAt(task.Pos);

                    if (m == 0 || m->Overlays.count() == 0) {
#ifdef DEBUG_CORE
                        qDebug() << "Fill empty TileMatrix: " + task.ToString() << " ID=" << debug;;
#endif // DEBUG_CORE

                        Tile *t = new Tile(task.Zoom, task.Pos);
                        QVector<MapType::Types> layers = OPMaps::Instance()->GetAllLayersOfType(GetMapType());

                        foreach(MapType::Types tl, layers) {
                            int retry = 0;

                            do {
                                QByteArray img;

                                // tile number inversion(BottomLeft -> TopLeft) for pergo maps
                                if (tl == MapType::PergoTurkeyMap) {
                                    img = OPMaps::Instance()->GetImageFrom(tl, Point(task.Pos.X(), maxOfTiles.Height() - task.Pos.Y()), task.Zoom);
                                } else { // ok
#ifdef DEBUG_CORE
                                    qDebug() << "start getting image" << " ID=" << debug;
#endif // DEBUG_CORE
                                    img = OPMaps::Instance()->GetImageFrom(tl, task.Pos, task.Zoom);
#ifdef DEBUG_CORE
                                    qDebug() << "Core::run:gotimage size:" << img.count() << " ID=" << debug << " time=" << t.elapsed();
#endif // DEBUG_CORE
                                }

                                if (img.length() != 0) {
                                    Moverlays.lock();
                                    {
                                        t->Overlays.append(img);
#ifdef DEBUG_CORE
                                        qDebug() << "Core::run append img:" << img.length() << " to tile:" << t->GetPos().ToString() << " now has " << t->Overlays.count() << " overlays" << " ID=" << debug;
#endif // DEBUG_CORE
                                    }
                                    Moverlays.unlock();

                                    break;
                                } else if (OPMaps::Instance()->RetryLoadTile > 0) {
#ifdef DEBUG_CORE
                                    qDebug() << "ProcessLoadTask: " << task.ToString() << " -> empty tile, retry " << retry << " ID=" << debug;;
#endif // DEBUG_CORE
                                    {
                                        QWaitCondition wait;
                                        QMutex m;
                                        m.lock();
                                        wait.wait(&m, 500);
                                    }
                                }
                            } while (++retry < OPMaps::Instance()->RetryLoadTile);
                        }

                        if (t->Overlays.count() > 0) {
                            Matrix.SetTileAt(task.Pos, t);
                            emit OnNeedInvalidation();

#ifdef DEBUG_CORE
                            qDebug() << "Core::run add tile " << t->GetPos().ToString() << " to matrix index " << task.Pos.ToString() << " ID=" << debug;
                            qDebug() << "Core::run matrix index " << task.Pos.ToString() << " as tile with " << Matrix.TileAt(task.Pos)->Overlays.count() << " ID=" << debug;
#endif // DEBUG_CORE
                        } else {
                            // emit OnTilesStillToLoad(tilesToload);

                            delete t;
                            t = 0;
                            emit OnNeedInvalidation();
                        }

                        // layers = null;
                    }
                }


                {
                    // last buddy cleans stuff ;}
                    if (last) {
                        OPMaps::Instance()->kiberCacheLock.lockForWrite();
                        OPMaps::Instance()->TilesInMemory.RemoveMemoryOverload();
                        OPMaps::Instance()->kiberCacheLock.unlock();

                        MtileDrawingList.lock();
                        {
                            Matrix.ClearPointsNotIn(tileDrawingList);
                        }
                        MtileDrawingList.unlock();


                        emit OnTileLoadComplete();


                        emit OnNeedInvalidation();
                    }
                }
            }
#ifdef DEBUG_CORE
            qDebug() << "loaderLimit release:" + loaderLimit.available() << " ID=" << debug;
#endif
            emit OnTilesStillToLoad(tilesToload < 0 ? 0 : tilesToload);
            loaderLimit.release();
        }
    }
    MrunningThreads.lock();
    --runningThreads;
    MrunningThreads.unlock();
}
diagnostics Core::GetDiagnostics()
{
    MrunningThreads.lock();
    diag = OPMaps::Instance()->GetDiagnostics();
    diag.runningThreads = runningThreads;
    MrunningThreads.unlock();
    return diag;
}

void Core::SetZoom(const int &value)
{
    if (!isDragging) {
        zoom = value;
        minOfTiles = Projection()->GetTileMatrixMinXY(value);
        maxOfTiles = Projection()->GetTileMatrixMaxXY(value);
        currentPositionPixel = Projection()->FromLatLngToPixel(currentPosition, value);
        if (started) {
            MtileLoadQueue.lock();
            tileLoadQueue.clear();
            MtileLoadQueue.unlock();
            MtileToload.lock();
            tilesToload = 0;
            MtileToload.unlock();
            Matrix.Clear();
            GoToCurrentPositionOnZoom();
            UpdateBounds();
            keepInBounds();
            emit OnMapDrag();
            emit OnMapZoomChanged();
            emit OnNeedInvalidation();
        }
    }
}

void Core::SetCurrentPosition(const PointLatLng &value)
{
    if (!IsDragging()) {
        currentPosition = value;
        SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(value, Zoom()));

        if (started) {
            GoToCurrentPosition();
            emit OnCurrentPositionChanged(currentPosition);
        }
    } else {
        currentPosition = value;
        SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(value, Zoom()));

        if (started) {
            emit OnCurrentPositionChanged(currentPosition);
        }
    }
}
void Core::SetMapType(const MapType::Types &value)
{
    if (value != GetMapType()) {
        mapType = value;

        switch (value) {
        case MapType::ArcGIS_Map:
        case MapType::ArcGIS_Satellite:
        case MapType::ArcGIS_ShadedRelief:
        case MapType::ArcGIS_Terrain:
        {
            if (Projection()->Type() != "PlateCarreeProjection") {
                SetProjection(new PlateCarreeProjection());
                maxzoom = 13;
            }
        }
        break;

        case MapType::ArcGIS_MapsLT_Map_Hybrid:
        case MapType::ArcGIS_MapsLT_Map_Labels:
        case MapType::ArcGIS_MapsLT_Map:
        case MapType::ArcGIS_MapsLT_OrtoFoto:
        {
            if (Projection()->Type() != "LKS94Projection") {
                SetProjection(new LKS94Projection());
                maxzoom = 11;
            }
        }
        break;

        case MapType::PergoTurkeyMap:
        {
            if (Projection()->Type() != "PlateCarreeProjectionPergo") {
                SetProjection(new PlateCarreeProjectionPergo());
                maxzoom = 17;
            }
        }
        break;

        case MapType::YandexMapRu:
        {
            if (Projection()->Type() != "MercatorProjectionYandex") {
                SetProjection(new MercatorProjectionYandex());
                maxzoom = 13;
            }
        }
        break;

        default:
        {
            if (Projection()->Type() != "MercatorProjection") {
                SetProjection(new MercatorProjection());
                maxzoom = 21;
            }
        }
        break;
        }

        minOfTiles = Projection()->GetTileMatrixMinXY(Zoom());
        maxOfTiles = Projection()->GetTileMatrixMaxXY(Zoom());
        SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(CurrentPosition(), Zoom()));

        if (started) {
            CancelAsyncTasks();
            OnMapSizeChanged(Width, Height);
            GoToCurrentPosition();
            ReloadMap();
            GoToCurrentPosition();
            emit OnMapTypeChanged(value);
        }
    }
}
void Core::StartSystem()
{
    if (!started) {
        started = true;

        ReloadMap();
        GoToCurrentPosition();
    }
}

void Core::UpdateCenterTileXYLocation()
{
    PointLatLng center = FromLocalToLatLng(Width / 2, Height / 2);
    Point centerPixel  = Projection()->FromLatLngToPixel(center, Zoom());

    centerTileXYLocation = Projection()->FromPixelToTileXY(centerPixel);
}

void Core::OnMapSizeChanged(int const & width, int const & height)
{
    Width  = width;
    Height = height;

    sizeOfMapArea.SetWidth(1 + (Width / Projection()->TileSize().Width()) / 2);
    sizeOfMapArea.SetHeight(1 + (Height / Projection()->TileSize().Height()) / 2);

    UpdateCenterTileXYLocation();

    if (started) {
        UpdateBounds();

        emit OnCurrentPositionChanged(currentPosition);
    }
}
void Core::OnMapClose()
{
    // if(waitOnEmptyTasks != null)
    // {
    // try
    // {
    // waitOnEmptyTasks.Set();
    // waitOnEmptyTasks.Close();
    // }
    // catch
    // {
    // }
    // }

    CancelAsyncTasks();
}
GeoCoderStatusCode::Types Core::SetCurrentPositionByKeywords(QString const & keys)
{
    GeoCoderStatusCode::Types status = GeoCoderStatusCode::Unknow;
    PointLatLng pos = OPMaps::Instance()->GetLatLngFromGeodecoder(keys, status);

    if (!pos.IsEmpty() && (status == GeoCoderStatusCode::G_GEO_SUCCESS)) {
        SetCurrentPosition(pos);
    }

    return status;
}
RectLatLng Core::CurrentViewArea()
{
    PointLatLng p = Projection()->FromPixelToLatLng(-renderOffset.X(), -renderOffset.Y(), Zoom());
    double rlng   = Projection()->FromPixelToLatLng(-renderOffset.X() + Width, -renderOffset.Y(), Zoom()).Lng();
    double blat   = Projection()->FromPixelToLatLng(-renderOffset.X(), -renderOffset.Y() + Height, Zoom()).Lat();

    return RectLatLng::FromLTRB(p.Lng(), p.Lat(), rlng, blat);
}
PointLatLng Core::FromLocalToLatLng(int const & x, int const & y)
{
    return Projection()->FromPixelToLatLng(Point(x - renderOffset.X(), y - renderOffset.Y()), Zoom());
}


Point Core::FromLatLngToLocal(PointLatLng const & latlng)
{
    Point pLocal = Projection()->FromLatLngToPixel(latlng, Zoom());

    pLocal.Offset(renderOffset);
    return pLocal;
}
int Core::GetMaxZoomToFitRect(RectLatLng const & rect)
{
    int zoom = 0;

    for (int i = 1; i <= MaxZoom(); i++) {
        Point p1 = Projection()->FromLatLngToPixel(rect.LocationTopLeft(), i);
        Point p2 = Projection()->FromLatLngToPixel(rect.Bottom(), rect.Right(), i);

        if (((p2.X() - p1.X()) <= Width + 10) && (p2.Y() - p1.Y()) <= Height + 10) {
            zoom = i;
        } else {
            break;
        }
    }

    return zoom;
}
void Core::BeginDrag(Point const & pt)
{
    dragPoint.SetX(pt.X() - renderOffset.X());
    dragPoint.SetY(pt.Y() - renderOffset.Y());
    isDragging = true;
}
void Core::EndDrag()
{
    isDragging = false;
    emit OnNeedInvalidation();
}
void Core::ReloadMap()
{
    if (started) {
#ifdef DEBUG_CORE
        qDebug() << "------------------";
#endif // DEBUG_CORE

        MtileLoadQueue.lock();
        {
            tileLoadQueue.clear();
        }
        MtileLoadQueue.unlock();
        MtileToload.lock();
        tilesToload = 0;
        MtileToload.unlock();
        Matrix.Clear();

        emit OnNeedInvalidation();
    }
}
void Core::GoToCurrentPosition()
{
    // reset stuff
    renderOffset = Point::Empty;
    centerTileXYLocationLast = Point::Empty;
    dragPoint    = Point::Empty;

    // goto location
    Drag(Point(-(GetcurrentPositionGPixel().X() - Width / 2), -(GetcurrentPositionGPixel().Y() - Height / 2)));
}
void Core::GoToCurrentPositionOnZoom()
{
    // reset stuff
    renderOffset = Point::Empty;
    centerTileXYLocationLast = Point::Empty;
    dragPoint    = Point::Empty;

    // goto location and centering
    if (MouseWheelZooming) {
        if (mousewheelzoomtype != MouseWheelZoomType::MousePositionWithoutCenter) {
            Point pt = Point(-(GetcurrentPositionGPixel().X() - Width / 2), -(GetcurrentPositionGPixel().Y() - Height / 2));
            renderOffset.SetX(pt.X() - dragPoint.X());
            renderOffset.SetY(pt.Y() - dragPoint.Y());
        } else { // without centering
            renderOffset.SetX(-GetcurrentPositionGPixel().X() - dragPoint.X());
            renderOffset.SetY(-GetcurrentPositionGPixel().Y() - dragPoint.Y());
            renderOffset.Offset(mouseLastZoom);
        }
    } else { // use current map center
        mouseLastZoom = Point::Empty;

        Point pt = Point(-(GetcurrentPositionGPixel().X() - Width / 2), -(GetcurrentPositionGPixel().Y() - Height / 2));
        renderOffset.SetX(pt.X() - dragPoint.X());
        renderOffset.SetY(pt.Y() - dragPoint.Y());
    }

    UpdateCenterTileXYLocation();
}
void Core::DragOffset(Point const & offset)
{
    renderOffset.Offset(offset);

    UpdateCenterTileXYLocation();

    if (centerTileXYLocation != centerTileXYLocationLast) {
        centerTileXYLocationLast = centerTileXYLocation;
        UpdateBounds();
    }

    {
        LastLocationInBounds = CurrentPosition();
        SetCurrentPosition(FromLocalToLatLng((int)Width / 2, (int)Height / 2));
    }

    emit OnNeedInvalidation();
    emit OnMapDrag();
}
void Core::Drag(Point const & pt)
{
    renderOffset.SetX(pt.X() - dragPoint.X());
    renderOffset.SetY(pt.Y() - dragPoint.Y());
    keepInBounds();
    UpdateCenterTileXYLocation();

    if (centerTileXYLocation != centerTileXYLocationLast) {
        centerTileXYLocationLast = centerTileXYLocation;
        UpdateBounds();
    }

    if (IsDragging()) {
        LastLocationInBounds = CurrentPosition();
        SetCurrentPosition(FromLocalToLatLng((int)Width / 2, (int)Height / 2));
    }

    emit OnNeedInvalidation();


    emit OnMapDrag();
}
void Core::CancelAsyncTasks()
{
    if (started) {
        ProcessLoadTaskCallback.waitForDone();
        MtileLoadQueue.lock();
        {
            tileLoadQueue.clear();
            // tilesToload=0;
        }
        MtileLoadQueue.unlock();
        MtileToload.lock();
        tilesToload = 0;
        MtileToload.unlock();
        // ProcessLoadTaskCallback.waitForDone();
    }
}
void Core::UpdateBounds()
{
    MtileDrawingList.lock();
    {
        FindTilesAround(tileDrawingList);

#ifdef DEBUG_CORE
        qDebug() << "OnTileLoadStart: " << tileDrawingList.count() << " tiles to load at zoom " << Zoom() << ", time: " << QDateTime::currentDateTime().date();
#endif // DEBUG_CORE

        emit OnTileLoadStart();


        foreach(Point p, tileDrawingList) {
            LoadTask task = LoadTask(p, Zoom());
            {
                MtileLoadQueue.lock();
                {
                    if (!tileLoadQueue.contains(task)) {
                        MtileToload.lock();
                        ++tilesToload;
                        MtileToload.unlock();
                        tileLoadQueue.enqueue(task);
#ifdef DEBUG_CORE
                        qDebug() << "Core::UpdateBounds new Task" << task.Pos.ToString();
#endif // DEBUG_CORE
                        ProcessLoadTaskCallback.start(this);
                    }
                }
                MtileLoadQueue.unlock();
            }
        }
    }
    MtileDrawingList.unlock();
    UpdateGroundResolution();
}
void Core::FindTilesAround(QList<Point> &list)
{
    list.clear();;
    for (int i = -sizeOfMapArea.Width(); i <= sizeOfMapArea.Width(); i++) {
        for (int j = -sizeOfMapArea.Height(); j <= sizeOfMapArea.Height(); j++) {
            Point p = centerTileXYLocation;
            p.SetX(p.X() + i);
            p.SetY(p.Y() + j);

            // if(p.X < minOfTiles.Width)
            // {
            // p.X += (maxOfTiles.Width + 1);
            // }

            // if(p.X > maxOfTiles.Width)
            // {
            // p.X -= (maxOfTiles.Width + 1);
            // }

            if (p.X() >= minOfTiles.Width() && p.Y() >= minOfTiles.Height() && p.X() <= maxOfTiles.Width() && p.Y() <= maxOfTiles.Height()) {
                if (!list.contains(p)) {
                    list.append(p);
                }
            }
        }
    }
}
void Core::UpdateGroundResolution()
{
    double rez = Projection()->GetGroundResolution(Zoom(), CurrentPosition().Lat());

    pxRes100m   = (int)(100.0 / rez); // 100 meters
    pxRes1000m  = (int)(1000.0 / rez); // 1km
    pxRes10km   = (int)(10000.0 / rez); // 10km
    pxRes100km  = (int)(100000.0 / rez); // 100km
    pxRes1000km = (int)(1000000.0 / rez); // 1000km
    pxRes5000km = (int)(5000000.0 / rez); // 5000km
}
void Core::keepInBounds()
{
    if (renderOffset.X() > 0) {
        renderOffset.SetX(0);
    }
    if (renderOffset.Y() > 0) {
        renderOffset.SetY(0);
    }
    int maxDragY = GetCurrentRegion().Height() - GettileRect().Height() * (maxOfTiles.Height() - minOfTiles.Height() + 1);
    int maxDragX = GetCurrentRegion().Width() - GettileRect().Width() * (maxOfTiles.Width() - minOfTiles.Width() + 1);

    if (maxDragY > renderOffset.Y()) {
        renderOffset.SetY(maxDragY);
    }
    if (maxDragX > renderOffset.X()) {
        renderOffset.SetX(maxDragX);
    }
}
}
